/*!
 * Jsl v1.0 - The JavaScript Library
 *   Basic core functions and extensions to built-in objects
 *   Copyright 2010, Mykhailo Stadnyk <mikhus@gmail.com>
 *   Released under the MIT, BSD and GPL licenses
 *   More information: http://jslproject.googlecode.com/
 *
 * Included 3d party solutions:
 * 
 *  Sizzle CSS Selector Engine - v1.0
 *    Copyright 2009, The Dojo Foundation
 *    Released under the MIT, BSD, and GPL Licenses.
 *    More information: http://sizzlejs.com/
 */
var Jsl = function( selector, id) {
	var obj = document.query ? document.query( selector) : null;
	if (obj && id) {
		obj.attr( 'id', id);
	}
	return obj;
};

/**
 * Copies all the properties of source object to destination object
 * and overrides if they already exist.
 * 
 * @param  {Object} dst - destinagtion object
 * @param  {Object} src - source object
 * @return {Object} returns dst
 */
Jsl.apply = function( dst, src) {
	if (dst && src && typeof src == 'object') {
		for (var p in src) {
			dst[p] = src[p];
		}
	}
	return dst;
};

/**
 * Free all attributes of a given elements (prepare to remove)
 */
Jsl.freeElement = function( d) {
    var a = d.attributes, i, l, n;
    if (a) {
        for (i = a.length - 1; i >= 0; i -= 1) {
            n = a[i].name;
            if (typeof d[n] === 'function') {
                d[n] = null;
            }
        }
    }
    a = d.childNodes;
    if (a) {
        l = a.length;
        for (i = 0; i < l; i += 1) {
        	Jsl.freeElement( d.childNodes[i]);
        }
    }
};

/**
 * Basic static core properties and methods
 */
Jsl.apply( Jsl, function() {
	var oids = 0,
		b    = [
			/opera/, /chrome/, /webkit/, /safari/, /applewebkit\/4/, /version\/3/,
			/version\/4/, /msie/, /msie 7/, /msie 8/, /gecko/, /rv:1\.8/, /rv:1\.9/,
			/windows|win32/, /macintosh|mac os x/, /adobeair/, /linux/, /msie 9/, /msie 10/
		],
		useOwn = !!{}.hasOwnProperty,
		b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
	;

	for (var i = 0, re; re = b[i]; i++)
		b[i] = re.test( navigator.userAgent.toLowerCase())
	;

	/**
	 * Fixing up checks
	 */
	b[3]  = b[1] && b[3];
	b[4]  = b[3] && b[4];
	b[4]  = b[3] && b[5];
	b[6]  = b[3] && b[6];
	b[7]  = !b[0] && b[7];
	b[8]  = b[7] && b[8];
	b[9]  = b[8] && b[9];
	b[10] = !b[2] && b[10];
	b[11] = b[10] && b[11];
	b[12] = b[10] && b[12];

	return {

	version : '1.0',

	/**
	 * Browser-specific info
	 */
    isOpera     : b[0],
    isChrome    : b[1],
    isWebKit    : b[2],
    isSafari    : b[3],
    isSafari2   : b[4],
    isSafari3   : b[5],
    isSafari4   : b[6],
    isIe        : b[7],
    isIe7       : b[8],
    isIe8       : b[9],
    isIe6       : b[7] && !b[8] && !b[9] && !b[17],
    isIe9       : b[17],
    isIe10      : b[18],
    isGecko     : b[10],
    isGecko2    : b[11],
    isGecko3    : b[12],
    isWindows   : b[13],
    isMac       : b[14],
    isAir       : b[15],
    isLinux     : b[16],

	id : function() { return 'jsl-oid-' + (++oids); },

	/**
     * Copies all the properties of source object to destination object
     * if they don't already exist.
     * 
     * @param  {Object} dst    - The destination object
     * @param  {Object} src    - The source object
     * @return {Object} returns dst
     */
	applyIf : function( dst, src) {
		if (dst) {
			for (var p in src)
				if ((!useOwn || src.hasOwnProperty( p)) && !Jsl.isDefined( dst[p]))
					dst[p] = src[p]
			;
		}
		return dst;
	},

	/**
	 * Adds a list of functions to the prototype of an existing class,
	 * overwriting any existing methods with the same name.
	 * 
	 * @param {Object} - original class
	 * @param {Object} - override methods
	 */
	override : function( origclass, overrides){
		if (overrides) {
			var p = origclass.prototype;
			Jsl.apply( p, overrides);
			if (Jsl.isIe && (!useOwn || overrides.hasOwnProperty( 'toString')))
				p.toString = overrides.toString
			;
        }
    },

    /**
     * OOP-like class definitions
     * 
     * @param {Function} - subclass constructor
     * @param {Function} - superclass constructor
     * @param {Object}   - overrides of subclass for superclass
     */
	extend : function() {
		var ovrd  = function( o) { for (var m in o) this[m] = o[m]; },
			cnstr = Object.prototype.constructor
		;

		return function( sub, sup, overrides) {
			if (Jsl.isObject( sup)) {
				overrides = sup;
				sup = sub;
				sub = overrides.constructor != cnstr ?
					overrides.constructor :
					function() { sup.apply( this, arguments); }
				;
			}

			var Construct = function() {},
				subp,
				supp = sup.prototype
			;

			Construct.prototype = supp;
			subp = sub.prototype = new Construct();

			/**
			 * Creating a valid toString representaion of an object
			 */
			subp.toString = function() {
				var str = this.constructor.toString();
				return '[object ' + (str.match( /([\w.]+?)\.\$?super(class)?\.constructor/)[1] || 'Object') + ']';
			};

			subp.constructor = sub;
			sub.superclass = sub.$super = supp;

			if (supp.constructor == cnstr)
				supp.constructor = sup
			;
			sub.override = function( o) {
				Jsl.override( sub, o);
			};
			subp.superclass = subp.$super = (function(){
				return supp;
			});

			subp.override = ovrd;
			Jsl.override( sub, overrides);
			sub.extend = function( o) { return Jsl.extend( sub, o); };

			return sub;
		};
	}(),

	/**
	 * Checks if given value is an Object
	 * 
	 * @param  {Mixed} - value to check
	 * @return {Boolean}
	 */
	isObject   : function( o) { return (!!o && Object.prototype.toString.call( o) === '[object Object]'); },

	/**
	 * Checks whenever a given object is a DOM element
	 * 
	 * @param  {Mixed} - value to check
	 * @return {Boolean}
	 */
	isElement  : function( o) { return !!(o && o.nodeType == 1); },

	/**
	 * Checks if given value is an array
	 * 
	 * @param  {Mixed} - value to check
	 * @return {Boolean}
	 */
	isArray    : function( a) { return a && (a instanceof Array); },

	/**
	 * Checks if given value is an instance of Regexp object
	 * 
	 * @param  {Mixed} r - value to check
	 * @return {Boolean}
	 */
	isRegExp   : function( r) { return (r instanceof RegExp); },

	/**
	 * Checks if given value is a function
	 * 
	 * @param  {Mixed} - value to check
	 * @return {Boolean}
	 */
	isFunction : function( f) { return f && (typeof f === 'function'); },

	/**
	 * Checks if given value is a string
	 * 
	 * @param  {Mixed} - value to check
	 * @return {Boolean}
	 */
	isString   : function( s) { return (typeof s === 'string'); },

	/**
	 * Checks if given value is numeric.
	 * 
	 * Note: will return true if value has type string but
	 * contains int or float value
	 * 
	 * @param  {Mixed} - value to check
	 * @return {Boolean}
	 */
	isNumeric  : function( n) { return (this.isNumber( n) || /^\d+(\.\d+)?$/.test( String( n))); },

	/**
	 * Checks if given value is a number
	 */
	isNumber   : function( n) { return (typeof n === 'number' && isFinite(n)); },

	/**
	 * Checks if given variable is not undefined
	 * 
	 * @param  {Mixed}
	 * @return {Boolean}
	 */
	isDefined  : function( o) { return (o !== undefined); },

	/**
	 * Checks if given variable is null
	 * 
	 * @param  {Mixed}
	 * @return {Boolean}
	 */
	isNull     : function( o) { return (o === null); },

	/**
	 * Checks if given value is Date object
	 * 
	 * @param  {Mixed}
	 * @return {Boolean}
	 */
	isDate     : function( d) { return (d instanceof Date); },

	/**
	 * Checks if given value is boolean
	 * 
	 * @param  {Mixed}
	 * @return {Boolean}
	 */
	isBoolean : function( o) { return (typeof o == 'boolean'); },

	/**
	 * Makes an Array from a given collection
	 * 
	 * @param  {Collection} collection
	 * @return {Array}
	 */
	arrize     : function( collection) {
		var a = [];
		if (collection && collection.length)
			for (var i = 0, s = collection.length; i < s; i++)
				a[i] = collection[i]
		;
		return a;
	},

	/**
	 * Primitive functions aliases
	 */
	fn : {
		empty      : function() {},
		emptyTrue  : function() { return true; },
		emptyFalse : function() { return false; }
	},

	/**
	 * JSON object
	 */
	Json : function() {
		var m = {
			"\b": '\\b', "\t": '\\t', "\n": '\\n', "\f": '\\f',
            "\r": '\\r', '"' : '\\"', "\\": '\\\\'
        };
		return {
			isNative  : (window.JSON && JSON.toString() == '[object JSON]'),
			stringify : function( o) {
				if (!Jsl.isDefined( o) || Jsl.isNull( o)) {
	                return "null";
				} else if (this.isNative) {
					try {
						return JSON.stringify( o);
					} catch( e) {
						this.isNative = false;
						var json = this.stringify( o);
						this.isNative = true;
						return json;
					}
				} else if (Jsl.isArray( o)) {
	            	var a = ["["], b, i, l = o.length, v;
	                for (i = 0; i < l; i++) {
	                    v = o[i];
	                    switch (typeof v) {
	                        case 'undefined':
	                        case 'function':
	                        case 'unknown':
	                            break;
	                        default:
	                            if (b) a.push(',');
	                            a.push( v === null ? 'null' : Jsl.Json.stringify( v));
	                            b = true;
	                    }
	                }
	                a.push( ']');
	                return a.join( '');
	            } else if (Jsl.isDate( o)) {
	            	return '"' + o.getFullYear() + "-" +
	                	(o.getMonth() + 1).pad() + "-" +
	                	o.getDate().pad() + "T" +
	                	o.getHours().pad() + ":" +
	                	o.getMinutes().pad() + ":" +
	                	o.getSeconds().pad() + '"'
	                ;
	            } else if(Jsl.isString( o)) {
	            	if (/["\\\x00-\x1f]/.test( o)) {
	                    return '"' + o.replace( /([\x00-\x1f\\"])/g, function( a, b) {
	                        var c = m[b];
	                        if (c) return c;
	                        c = b.charCodeAt();
	                        return "\\u00" + Math.floor( c / 16).toString( 16) + (c % 16).toString( 16);
	                    }) + '"';
	                }
	                return '"' + o + '"';
	            } else if (Jsl.isNumber( o)) {
	                return Jsl.isNumeric( o) ? new String( o) : 'null';
	            } else if (Jsl.isBoolean( o)) {
	                return new String( o);
	            } else {
	                var a = ["{"], b, i, v;
	                for (i in o) {
	                    if (!o.getElementsByTagName) {
	                        if ((!useOwn || o.hasOwnProperty( i))) {
	                            v = o[i];
	                            switch (typeof v) {
	                            	case 'undefined':
	                            	case 'function':
	                            	case 'unknown': {
	                                	break;
	                            	}
	                            	default: {
	                                	if (b) a.push( ',');
	                                	a.push(
	                                		Jsl.Json.stringify( i),
	                                		':',
	                                		v === null ? 'null' : Jsl.Json.stringify( v)
	                                	);
	                                	b = true;
	                                	break;
	                            	}
	                            }
	                        }
	                    }
	                }
	                a.push( '}');
	                return a.join( '');
	            }
			}
		};
	}(),

	/**
	 * Encodes given object to JSON string
	 * Actually shorthand for Jsl.Json.stringify
	 * 
	 * @param  {Object} - any supported object
	 * @return {String} - JSON string
	 */
	encode : function( o) { return this.Json.stringify( o); },

	/**
	 * Decodes given JSON string value to an object
	 * 
	 * @param  {String} - JSON string
	 * @return {Object} - any supported object
	 */
	decode : function( str) {
		if (str == '') {
			str = '""';
		}

		str = str.replace( /(\{|\}|\[|\]|,)\s+(\S)/g, '$1$2');

		return eval( '(' + str + ')');
	},

	/**
	 * Encodes string to be compatible with URL syntax
	 * 
	 * @param  {String}
	 * @return {String}
	 */
	urlEncode : function( str, phpCompat) {
		var str = str.replace(/\r\n/g, '\n'),
			utf = Jsl.utfEncode( str)
		;
		return phpCompat ?
			escape( utf)
				.replace( /!/g, '%21')
				.replace( /'/g, '%27')
				.replace( /\(/g, '%28')
				.replace( /\)/g, '%29')
				.replace( /\*/g, '%2A')
				.replace( /%20/g, '+') :
			escape( utf)
		;
	},

	/**
	 * Decodes URL-encoded string
	 * 
	 * @param  {String}
	 * @return {String}
	 */
	urlDecode : function( str, phpCompat) {
		var str = unescape(
				phpCompat ?
				str
					.replace( /%21/g, '!')
					.replace( /%27/g, "'")
					.replace( /%28/g, '(')
					.replace( /%29/g, ')')
					.replace( /%2A/g, '*')
					.replace( /\+/g, '%20') :
				str
			),
			i = 0, c = c1 = c2 = 0
		;
		return Jsl.utfDecode( str);
	},

	/**
	 * Encoding given string with MIME BASE64 algorithm
	 * 
	 * Initially found on http://phpjs.org, little bit optimized and adopted
	 * 
	 * @param  {String}
	 * @return {String}
	 */
	base64Encode : function( data) {
		var
	    	o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc = '', arr = [],
	    	data = Jsl.utfEncode( data + ''), len = data.length
	    ;
	    if (!data) return data;
	    do {
	        o2 = data.charCodeAt( i++);
	        o3 = data.charCodeAt( i++);
	 
	        bits = o1<<16 | o2<<8 | o3; 
	        h1 = bits>>18 & 0x3f;
	        h2 = bits>>12 & 0x3f;
	        h3 = bits>>6 & 0x3f;
	        h4 = bits & 0x3f; 
	        arr[ac++] = b64.charAt( h1) + b64.charAt( h2) + b64.charAt( h3) + b64.charAt( h4);
	    } while (i < len);
	    enc = arr.join( '');
	    switch (len % 3) {
	        case 1: enc = enc.slice( 0, -2) + '=='; break;
	        case 2: enc = enc.slice( 0, -1) + '='; break;
	    }
	    return enc;
	},

	/**
	 * Decodes a given string encoded with MIME BASE64 algorithm
	 * 
	 * Initially found on http://phpjs.org, little bit optimized and adopted
	 * 
	 * @param  {String}
	 * @return {String}
	 */
	base64Decode : function( data) {
		var
	    	o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, dec = '', arr = [],
	    	data = (data + ''), len = data.length
	    ;
	    if (!data) return data;
	    do {
	        h1 = b64.indexOf( data.charAt( i++));
	        h2 = b64.indexOf( data.charAt( i++));
	        h3 = b64.indexOf( data.charAt( i++));
	        h4 = b64.indexOf( data.charAt( i++));
	
	        bits = h1<<18 | h2<<12 | h3<<6 | h4;
	        o1 = bits>>16 & 0xff;
	        o2 = bits>>8 & 0xff;
	        o3 = bits & 0xff;
	
	        if (h3 == 64)
				arr[ac++] = String.fromCharCode( o1)
			; else if (h4 == 64)
	            arr[ac++] = String.fromCharCode( o1, o2)
	        ; else
				arr[ac++] = String.fromCharCode( o1, o2, o3)
			;
	    } while (i < len);
	    return Jsl.utfDecode( arr.join( ''));
	},

	/**
	 * Encodes an ISO-8859-1 string to UTF-8
	 * 
	 * Initially found on http://phpjs.org, little bit optimized and adopted
	 * 
	 * @param  {String}
	 * @return {String}
	 */
	utfEncode : function( str) {
		var str = str + '', utftext = '', start = 0, end = 0, len = str.length; 
	    for (var n = 0; n < len; n++) {
	        var c1 = str.charCodeAt( n), enc = null;
	        if (c1 < 128)
	            end++
	        ; else if (c1 > 127 && c1 < 2048)
				enc = String.fromCharCode((c1 >> 6) | 192) + String.fromCharCode((c1 & 63) | 128)
			; else
	            enc = String.fromCharCode((c1 >> 12) | 224) + String.fromCharCode(((c1 >> 6) & 63) | 128) + String.fromCharCode((c1 & 63) | 128)
	        ;
	        if (enc !== null) {
				if (end > start)
	                utftext += str.substring( start, end)
	            ;
	            utftext += enc;
	            start = end = n + 1;        }
	    }
	    if (end > start)
	        utftext += str.substring( start, str.length)
	    ;
	    return utftext;
	},

	/**
	 * Decodes a UTF-8 encoded string into ISO-8859-1 string
	 * 
	 * Initially found on http://phpjs.org, little bit optimized and adopted
	 * 
	 * @param  {String}
	 * @return {String}
	 */
	utfDecode : function( str) {
		var str = (str + ''), arr = [], i = 0, ac = 0, c1 = 0, c2 = 0, c3 = 0, len = str.length;
	    while (i < len) {
	        c1 = str.charCodeAt( i);
	        if (c1 < 128) {
	            arr[ac++] = String.fromCharCode( c1);
	            i++;
	        } else if ((c1 > 191) && (c1 < 224)) {
	            c2 = str.charCodeAt( i + 1);
	            arr[ac++] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
	            i += 2;
	        } else {
	            c2 = str.charCodeAt( i + 1);
	            c3 = str.charCodeAt( i + 2);
	            arr[ac++] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
	            i += 3;
	        }
	    }
	    return arr.join( '');
	},

	/**
	 * Iterating an array or object properties with calling
	 * a user defined function each iteration
	 * 
	 * Params passed to a function are:
	 *  - Array/Object element value
	 *  - Array/Object element key
	 *  - iteration index number
	 * 
	 * @param {Object|Array|HTMLElement} o
	 * @param {Function} fn
	 */
	each : function( o, fn, scope) {
		var scope = scope || o || window;
		if (!Jsl.isFunction( fn) || !(Jsl.isArray( o) || Jsl.isObject( o) || Jsl.isElement( o)))
			return
		;
		if (Jsl.isElement( o)) {
			o = [o];
		}
		if (Jsl.isArray( o) || o == document.all) {
			for (var i = 0, s = o.length; i < s; i++) {
				var res = fn.call( scope, o[i], i, i);
				if (res === false) break;
			}
		} else if (typeof o == 'object') {
			var cnt = 0;
			for (var key in o) {
				try {
					if (!useOwn || o.hasOwnProperty( key)) {
						var res = fn.call( scope, o[key], key, cnt); cnt++;
						if (res === false) break;
					}
				} catch (err) {}
			}
		}
	},

	/**
	 * Cookies API
	 */
	Cookie : {

		/**
		 * Sets a new cookie or updates an existing
		 * 
		 * @param {String} name    - variable name of a cookie
		 * @param {String} value   - value of the cookie variable
		 * @param {String} expires - expiration date for the cookie
		 * @param {String} path    - path to the page to which cookie is set
		 * @param {String} domain  - the domain name for which cookie is set
		 * @param {Boolean} secure - is cookie should be used on secured protocol (HTTPS)
		 */
		set : function( name, value, expires, path, domain, secure) {
			document.cookie = name + "=" + escape( value) +
				((expires) ? "; expires=" + expires : "") +
				((path)    ? "; path=" + path : "") +
				((domain)  ? "; domain=" + domain : "") +
				((secure)  ? "; secure" : "")
			;
		},

		/**
		 * Returns the value stored within the cookie of given name
		 * 
		 * @param  {String} name  - the name of a cookie variable
		 * @return {String} value - the value stored within the cookie variable
		 */
		get : function( name) {
			var cookie = " " + document.cookie,
				search = " " + name + "=",
				setStr = null,
				offset = 0,
				end = 0
			;
			if (cookie.length > 0) {
				offset = cookie.indexOf( search);
				if (offset != -1) {
					offset += search.length;
					end = cookie.indexOf(";", offset)
					if (end == -1) {
						end = cookie.length;
					}
					setStr = unescape( cookie.substring( offset, end));
				}
			}

			return setStr;
		}
	}
};}());

/**
 * Extending String built-in object
 */
Jsl.applyIf( String.prototype, {

	/**
	 * Trims whitespaces from a both sides of a string
	 * 
	 * @return {String} - trimmed string
	 */
	trim : function() { return this.replace( /(^(\s|\u00A0)+|(\s|\u00A0)+$)/g, ''); },

	/**
	 * Trims whitespaces from a left side of a string
	 * 
	 * @return {String} - trimmed string
	 */
	ltrim : function() { return this.replace( /^(\s|\u00A0)+/, ''); },

	/**
	 * Trims whitespaces from a right side of a string
	 * 
	 * @return {String} - trimmed string
	 */
	rtrim : function() { return this.replace( /(\s|\u00A0)+$/,''); },

	/**
	 * Repeats current string given number times
	 * 
	 * @param  {Integer} num - numberof times to repeat
	 * @return {String}
	 */
	repeat : function( num) {
		var self = this,
			num = num && num > 0 ? num : 1,
			i = 1
		;
		for (; i < num; i++)
			self += this
		;
		return self;
	}
});

/**
 * Extending Array built-in object
 */
Jsl.applyIf( Array.prototype, {

	/**
	 * Checks if given value is present into current array
	 * 
	 * @param  {Mixed} val
	 * @return {Boolean}
	 */
	inArray: function( val) {
		for (var i = 0, s = this.length; i < s; i++)
			if (this[i] == val)
				return true
		;
		return false;
	},

	/**
	 * Megre current array with one or more other arrays
	 * 
	 * NOTE: this method modifying current array as well,
	 *       if you do not need - use native concat method instead
	 * 
	 * @param  {Array},.. - array(s) to merge with current array
	 * @return {Array}    - result array
	 */
	merge : function() {
		for (var i = 0, s = arguments.length; i < s; i++) {
			var arr = arguments[i];
			if (Jsl.isArray( arr))
				for (var ii = 0, ss = arr.length; ii < ss; ii++)
					this[this.length] = arr[ii]
			; else
				throw new Error( 'Argument ' + i + ' passed to Array.merge() is not an array!')
			;
		}
		return this;
	},

	/**
	 * Makes an array containing unique values from current array
	 * 
	 * @return {Array}
	 */
	unique : function() {
		var a = [];
		for (var i = 0, s = this.length; i < s; i++)
			if (!a.inArray( this[i]))
				a[a.length] = this[i]
		;
		return a;
	}
});

/**
 * Extending Function built-in object
 */
Jsl.applyIf( Function.prototype, {

	/**
	 * Binding current function to a given context with a given arguments
	 * 
	 * @param  {Object}   - context scope for function
	 * @param  {Mixed},.. - arguments to bypass to a function
	 * @return {Function}
	 */
	bind : function() {
		if (!arguments.length) return this;
		var args  = Jsl.arrize( arguments),
			scope = args.shift() || window,
			self  = this
		;
		return function() {
			args = args.merge( Jsl.arrize( arguments));
			return self.apply( scope, args);
		};
	},

	defer : function() {
		if (!arguments.length) return this;
		var args  = Jsl.arrize( arguments),
			mills = args.shift(),
			scope = args.shift() || window,
			self  = this,
			fn    = function() { self.apply( scope, args); }
		;
		if (mills > 0)
			return setTimeout( fn, mills)
		;
		fn();
		return 0;
	}

});

/**
 * Extending Number built-in object
 */
Jsl.applyIf( Number.prototype, {
	/**
	 * Fill a number with leading zeros
	 * 
	 * @param  {Integer} num - [Optional]: number of leading zeros to add, default 1
	 * @return {Number}
	 */
	pad : function( num, base) {
		var base = base || 10,
			num  = num || 1
		;
		return this < base ? '0'.repeat( num) + String( this) : String( this);
	}
});
